﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

namespace ErrorDumper.Dumping {

    public static class ExtendedDump {

        public static object Combine(params object[] dump) {
            return dump;
        }

        public static IDictionary<string, object> ToDictionary(object dump) {
            IDictionary<string, object> result = null;
            _ToDictionary(dump, ref result);
            return result;
        }

        public static IDictionary<string, object> ToDictionary(params object[] dump) {
            IDictionary<string, object> result = null;
            _ToDictionary(dump, ref result);
            return result;
        }

        static void WriteEntry(object key, object value, ref IDictionary<string, object> dump) {
            if (dump == null)
                dump = new Dictionary<string, object>();

            string name = null;
            if (key != null)
                try {
                    name = key.ToString();
                }
                catch {
                    do
                        name = Guid.NewGuid().ToString();
                    while (dump.ContainsKey(name));
                }

            if (string.IsNullOrEmpty(name))
                name = Guid.NewGuid().ToString();

            dump[name] = value;
        }

        static void _ToDictionary(object target, ref IDictionary<string, object> dump) {
            if (target == null)
                return;

            var named = target as IDictionary<string, object>;
            if (named != null) {
                foreach (var item in named)
                    WriteEntry(item.Key, item.Value, ref dump);
                return;
            }

            var dict = target as IDictionary<object, object>;
            if (dict != null) {
                foreach (var item in dict)
                    WriteEntry(item.Key, item.Value, ref dump);
                return;
            }

            var coll = target as IEnumerable;
            if (coll != null) {
                foreach (var item in coll)
                    _ToDictionary(item, ref dump);
                return;
            }

            if (target is DictionaryEntry) {
                var entry = (DictionaryEntry)target;
                WriteEntry(entry.Key, entry.Value, ref dump);
                return;
            }

            var type = target.GetType();
            if (type.IsValueType && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) {
                var key = type.InvokeMember("Key", BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public, null, target, null);
                var value = type.InvokeMember("Value", BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public, null, target, null);
                WriteEntry(key, value, ref dump);
                return;
            }

            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(target)) {
                object value;
                try {
                    value = prop.GetValue(target);
                }
                catch (Exception ex) {
                    value = ex;
                }
                WriteEntry(prop.Name, value, ref dump);
            }
        }

        public static IDictionary<string, object> FromMetadata(object instance) {
            if (instance == null)
                return null;

            Type type = instance.GetType();
            Type attrType = typeof(ExtendedDumpMemberAttribute);
            IDictionary<string, object> dump = null;

            foreach (MemberInfo member in type.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.InvokeMethod)) {
                var attr = Attribute.GetCustomAttribute(member, attrType) as ExtendedDumpMemberAttribute;
                if (attr == null)
                    continue;

                var name = member.Name;
                if (!string.IsNullOrEmpty(attr.Name))
                    name = attr.Name;

                object value = null;
                try {
                    switch (member.MemberType) {
                        case MemberTypes.Field:
                            value = ((FieldInfo)member).GetValue(instance);
                            break;
                        case MemberTypes.Property:
                            value = ((PropertyInfo)member).GetValue(instance, null);
                            break;
                        case MemberTypes.Method:
                            value = ((MethodInfo)member).Invoke(instance, null);
                            break;
                    }

                    var dumpable = value as IExtendedDumpAccessor;
                    if (dumpable != null)
                        value = dumpable.GetExtendedDump();
                }
                catch (Exception ex) {
                    value = ex;
                }

                WriteEntry(name, value, ref dump);
            }

            return dump;
        }
    }
}